/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.j4sc.service;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import com.googlecode.j4sc.UserCredentials;
import com.googlecode.j4sc.conversion.Conversion;
import com.googlecode.j4sc.service.SubmissionServiceException.ServiceError;
import com.googlecode.j4sc.submission.MultiPartMimeBodyBuilder;
import com.googlecode.j4sc.submission.Submission;
import com.googlecode.j4sc.submission.SubmissionReference;

/**
 * The Service is used to {@link #submit(Submission)} to the SpinVox Create
 * Service for processing and {@link #retrieve(SubmissionReference)} a
 * {@link Conversion} which contains the converted text.
 * <p>
 * The {@link #DEVELOPMENT} and {@link #LIVE} static instances are provided for
 * convenience for submitting and retrieving from the two version of the
 * service.
 * 
 * @author Raj Patel, Dariusz Lorenc
 */
public class Service implements IService {

    /**
     * The SpinVox Create development service for sending free test requests
     */
    public static final Service DEVELOPMENT = createQuietly("https://dev.api.spinvox.com");

    /**
     * The SpinVox Create live service for submitting conversion requests
     * (charged from your conversion credits)
     */
    public static final Service LIVE = createQuietly("https://live.api.spinvox.com");

    private static final int CONNECTION_TIMEOUT = 10000;
    private static final int PORT = 443;
    private static final String REALM = "spinvoxapi";

    private final URI uri;
    private int responseTimeoutInMillis = 60000;

    private static Service createQuietly(String uri) {
        try {
            return new Service(new URI(uri));
        } catch (URISyntaxException e) {
            return null;
        }
    }

    /**
     * Construct service instance for provided URI
     * 
     * @param uri
     * @throws URISyntaxException
     *             thrown if the URI is syntactically invalid
     */
    public Service(URI uri) throws URISyntaxException {
        this.uri = uri;
    }

    /**
     * Gets instance of URI of the service
     * 
     * @return instance of URI of the service
     */
    public URI getUri() {
        return uri;
    }

    /**
     * Sets the response timeout for the service, default is 1 min (60000 ms)
     * 
     * @param timeoutInMillis
     *            the timeout in milliseconds
     */
    public void setResponseTimeout(int timeoutInMillis) {
        this.responseTimeoutInMillis = timeoutInMillis;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.googlecode.j4sc.service.IService#submit(com.googlecode.j4sc.submission
     * .Submission)
     */
    public SubmissionReference submit(Submission submission) throws ServiceException, IOException {
        if (submission == null) {
            throw new IllegalArgumentException("Submission must not be null");
        }
        HttpPost httppost = new HttpPost(uri);
        setHeaders(httppost);
        httppost.setEntity(new CustomStringEntity(submission.createMimeBody(), "multipart/mixed; " + "boundary=\""
                + MultiPartMimeBodyBuilder.MIME_BOUNDARY + "\""));

        HttpResponse response = executeHttpMethod(httppost, submission.getUserCredentials());

        SubmissionResult result = new SubmissionResult(response);

        return new SubmissionReference(result.getLocationUri(), submission.getUserCredentials(), submission
                .getReference());
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.googlecode.j4sc.service.IService#retrieve(com.googlecode.j4sc.submission
     * .SubmissionReference)
     */
    public Conversion retrieve(SubmissionReference reference) throws ServiceException, IOException {
        if (reference == null) {
            throw new IllegalArgumentException("SubmissionReference must not be null");
        }
        HttpGet httpget = new HttpGet(reference.getUri());
        HttpResponse response = executeHttpMethod(httpget, reference.getUserCredentials());

        return new RetrieveResult(response).getConversion();
    }

    private HttpResponse executeHttpMethod(HttpRequestBase httpRequest, UserCredentials userCredentials)
            throws IOException {
        DefaultHttpClient httpclient = new DefaultHttpClient();

        if (userCredentials != null) {
            configureAuthentication(userCredentials, httpclient);
        }
        setTimeouts(httpclient);

        return httpclient.execute(httpRequest);
    }

    private void setTimeouts(DefaultHttpClient httpclient) {
        HttpParams params = httpclient.getParams();
        HttpConnectionParams.setSoTimeout(params, responseTimeoutInMillis);
        HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
    }

    private void configureAuthentication(UserCredentials userCredentials, DefaultHttpClient httpclient) {
        Credentials defaultcreds = new UsernamePasswordCredentials(userCredentials.getUsername(), userCredentials
                .getPassword());
        httpclient.getCredentialsProvider().setCredentials(new AuthScope(uri.getHost(), PORT, REALM), defaultcreds);
    }

    private void setHeaders(HttpPost httppost) {
        httppost.setHeader("Connection", "keep-alive");
        httppost.setHeader("User-Agent", "j4sc");
        httppost.setHeader("Pragma", "no-cache");
        httppost.setHeader("Cache-control", "no-cache");
        httppost.setHeader("MIME-version", "1.0");
        httppost.setHeader("Date", DateUtils.formatDate(new Date()));
    }

    private static class CustomStringEntity extends StringEntity {

        public CustomStringEntity(String content, String contentType) throws UnsupportedEncodingException {
            super(content);
            super.setContentType(new BasicHeader("Content-Type", contentType));
        }
    }

    private static class SubmissionResult {

        private URI locationUri;

        private enum ResponseHeader {
            ERROR("X-Error"), LOCATION("Location");

            private final String headerName;

            private ResponseHeader(String headerName) {
                this.headerName = headerName;
            }

            private String getHeaderName() {
                return headerName;
            }
        }

        public SubmissionResult(HttpResponse response) throws ServiceException {
            int statusCode = response.getStatusLine().getStatusCode();

            if (statusCode == HttpStatus.SC_ACCEPTED) {
                locationUri = extractLocationUri(response);
            } else {
                throw new SubmissionServiceException(extractServiceError(response));
            }
        }

        private URI getLocationUri() {
            return locationUri;
        }

        private URI extractLocationUri(HttpResponse response) throws ServiceException {
            try {
                return new URI(extractSingleHeaderValue(ResponseHeader.LOCATION, response));
            } catch (URISyntaxException e) {
                throw new ServiceException(response.getStatusLine().getStatusCode(), String.format(
                        "Service returned invalid location URI - %s", e.getMessage()));
            }
        }

        private ServiceError extractServiceError(HttpResponse response) throws ServiceException {
            String errorValue = extractSingleHeaderValue(ResponseHeader.ERROR, response);
            // remove hyphens to convert error value to enum constant
            return ServiceError.valueOf(errorValue.replace("-", StringUtils.EMPTY));
        }

        private String extractSingleHeaderValue(ResponseHeader header, HttpResponse response) throws ServiceException {
            Header[] errorHeaders = response.getHeaders(header.getHeaderName());
            if (ArrayUtils.isEmpty(errorHeaders)) {
                throw new ServiceException(response.getStatusLine().getStatusCode(), String.format(
                        "Service response is missing [%s] header", header.headerName));
            }
            return errorHeaders[0].getValue().trim();
        }
    }

    private static class RetrieveResult {

        private Conversion conversion;

        public RetrieveResult(HttpResponse response) throws IOException, ServiceException {
            int statusCode = response.getStatusLine().getStatusCode();
            String reasonPhrase = response.getStatusLine().getReasonPhrase();
            String xml = IOUtils.toString(response.getEntity().getContent());
            conversion = new Conversion(statusCode, reasonPhrase, xml);
        }

        private Conversion getConversion() {
            return conversion;
        }
    }
}